💡 AI 인사이트

🤖 AI가 여기에 결과를 출력합니다...

댓글 커뮤니티

쿠팡이벤트

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

검색

    로딩 중이에요... 🐣

    [코담] 웹개발·실전 프로젝트·AI까지, 파이썬·장고의 모든것을 담아낸 강의와 개발 노트

    24 보안 | ✅ 저자: 이유정(박사)

    JWT란? JWT (JSON Web Token) 는 이런 인증 토큰을 JSON 형식으로 만들어 암호화한 것입니다. 구조는 3가지 조각으로 나뉩니다:

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
    eyJzdWIiOiJ1c2VyMSIsImV4cCI6MTYzNzk5Njk4Mn0.
    7kAgCj...암호화된서명...
    

    Header 토큰의 종류, 사용한 암호화 알고리즘 Payload 사용자 정보 (ex: id, 이름, 권한 등) Signature 암호화된 서명 (위조 방지용)

    JWT vs 일반 토큰 (Session Token 등) 비교
    항목 일반 토큰 (세션 기반) JWT (JSON Web Token)
    저장 위치 서버 메모리 (세션 스토리지) 클라이언트에 저장
    서버에서 토큰 관리 필요함 (세션 테이블 유지) 필요 없음 (Stateless)
    구조 단순 문자열 (의미 없음) 구조화된 JSON + 서명
    위변조 방지 불가능 (서명 없음) 가능 (서명 포함, 변조 시 검증 실패)
    확장성 서버가 커지면 세션 동기화 문제 생김 수평 확장에 유리 (Stateless 구조)
    단점 서버 메모리 부담, 유지 필요 탈취당하면 누구나 사용 가능 (주의 필요)

    보안 측면에서 JWT는 왜 유리할까?

    1. 위조 방지:
      JWT는 서명이 포함되어 있어, 누가 내용을 바꾸면 검증에 실패합니다.

    2. 서버가 기억 안 해도 됨:
      사용자가 보내는 토큰만 있으면, 다시 로그인 없이 확인 가능.
      서버는 "이 토큰이 위조된 게 아닌지만" 확인하면 됨.

    3. 토큰 안에 정보 포함 가능:
      예: {"username": "eunice", "role": "admin"}
      → 권한, 유저 정보도 포함 가능

    JWT는 "서명된 JSON 토큰"으로, 서버가 사용자를 식별하고 권한을 판단할 수 있게 해주는 위조 불가능한 인증표입니다.
    일반 토큰보다 보안성과 확장성이 뛰어나며, 서버에 상태를 저장하지 않아도 됩니다.

    Django나 Google API, Amazon Cloud에서 내려받는 보안키(비밀 키, API 키 등)와는 다른 역할:
    항목 JWT 보안 키(API Key / Secret Key)
    정의 사용자 정보를 담고 있는 토큰 (디지털 신분증) 서버끼리 통신 시, 인증을 위한 비밀키
    형태 aaaaa.bbbbb.ccccc 같은 문자열 sk_test_xxxxx / AIzaSy... 같은 문자열
    누가 만듬 서버가 사용자 로그인 후 발급 서비스 제공자(Google, AWS 등)가 발급
    용도 로그인된 유저의 상태를 확인 외부 API 인증(예: Stripe, Google Maps 등)
    보관 위치 클라이언트(브라우저/앱)에 저장 서버(환경변수 등)에만 저장
    예시 FastAPI에서 로그인 후 JWT 발급 .envSECRET_KEY=xxxxx 등 저장

    패키지 설치

    pip install fastapi uvicorn python-jose[cryptography] passlib[bcrypt]
    

    디렉토리 구조 (간단 예시)

    project/
    │
    ├── main.py
    ├── auth.py
    └── users.py
    

    JWT 관련 설정 (auth.py)

    from fastapi import FastAPI, HTTPException, Depends, status, Form
    from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
    from passlib.context import CryptContext
    from jose import JWTError, jwt, ExpiredSignatureError
    from datetime import datetime, timedelta, timezone
    from typing import Optional
    from pydantic import BaseModel
    import os
    from dotenv import load_dotenv
    
    # ===============================
    # 📌 설정
    # ===============================
    load_dotenv()  # .env 파일 로드
    
    SECRET_KEY = os.getenv("SECRET_KEY", "mysecretkey")  # .env에서 가져오거나 기본값 사용
    ALGORITHM = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES = 30
    
    pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
    oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
    
    app = FastAPI()
    
    # ===============================
    # 📌 유저 모델 (Pydantic)
    # ===============================
    class User(BaseModel):
        username: str
    
    class UserInDB(User):
        hashed_password: str
    
    # ===============================
    # 📌 유저 저장소 (예제용)
    # ===============================
    fake_user_db = {
        "testuser": UserInDB(
            username="testuser",
            hashed_password=pwd_context.hash("test123")  # 앱 시작 시 해싱
        )
    }
    
    # ===============================
    # 📌 유틸 함수들
    # ===============================
    def verify_password(plain_password: str, hashed_password: str) -> bool:
        return pwd_context.verify(plain_password, hashed_password)
    
    def get_password_hash(password: str) -> str:
        return pwd_context.hash(password)
    
    def authenticate_user(username: str, password: str) -> Optional[UserInDB]:
        user = fake_user_db.get(username)
        if not user:
            return None
        if not verify_password(password, user.hashed_password):
            return None
        return user
    
    def create_access_token(data: dict, expires_delta: timedelta = None) -> str:
        to_encode = data.copy()
        expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
        to_encode.update({"exp": expire, "sub": data.get("sub")})
        return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    
    # ===============================
    # 📌 토큰 검증
    # ===============================
    def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
        try:
            payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
            username: str = payload.get("sub")
            if username is None:
                raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
            user = fake_user_db.get(username)
            if user is None:
                raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
            return user
        except ExpiredSignatureError:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
        except JWTError:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
    
    
    
    

    ✅ .env 예시 파일

     SECRET_KEY=your_super_secret_key_here
    

    위 코드를 실행하려면 실제 .env 파일을 프로젝트 루트에 두고 SECRET_KEY를 작성해야 합니다.

    ⚠️ 실제 운영 환경에서는 OpenSSL 등을 이용해 생성한 복잡한 키를 사용하세요.

    ⚠️ 예: openssl rand -hex 32


    FastAPI 앱 구성 (main.py)

    from fastapi import FastAPI, Depends, HTTPException, status
    from fastapi.security import OAuth2PasswordRequestForm
    from auth import User, authenticate_user, create_access_token, get_current_user
    
    app = FastAPI()
    
    @app.post("/token")
    def login(form_data: OAuth2PasswordRequestForm = Depends()):
        user = authenticate_user(form_data.username, form_data.password)
        
        if not user:
            raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect username or password")
        access_token = create_access_token(data={"sub": user.username})
        return {"access_token": access_token, "token_type": "bearer"}
    
      
      
    
    # ===============================
    # 📌 보호된 라우트
    # ===============================
    @app.get("/protected")
    def read_users_me(current_user: User = Depends(get_current_user)):
        return {"username": current_user.username}
    

    테스트 http

    # 🔐 auth.http - FastAPI 인증 테스트 파일
    
    ##
    @access_token = 발급토큰입력
    
    
    ### ✅ 1. 토큰 발급 요청 (로그인)
    POST http://127.0.0.1:8000/token
    Content-Type: application/x-www-form-urlencoded
    
    username=testuser&password=test123
      
    ### ✅ 2. 보호된 API 호출 - 내 정보 가져오기 (/users/me)
    GET http://127.0.0.1:8000/protected
    Authorization: Bearer {{access_token}}
    

    테스트 순서 로그인 (토큰 발급)

    curl -X POST "http://127.0.0.1:8000/token" \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "username=testuser&password=test123"
    

    응답:

    {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "token_type": "bearer"
    }
    

    보호된 경로 접근 테스트

    curl -X GET "http://127.0.0.1:8000/protected" \
     -H "Authorization: Bearer <위에서 받은 access_token>"
    

    응답:

    {
      "msg": "Hello testuser, you're authenticated!"
    }
    
    • /token으로 로그인 요청 (username/password)

    • 서버는 JWT 토큰을 발급

    • /protected 요청 시 토큰이 있어야 접근 가능

    • SECRET_KEY는 환경변수로 관리하세요.

    • 토큰 탈취 방지 위해 HTTPS + 토큰 저장 위치 고려 (쿠키 or localStorage)

    TOP
    preload preload